import { config } from './config'
import screenfull from './screenfull'

const bus = {
    all: new Map(),
    eTypes: {
        render_page: 'e1',
        exec_task: 'e2'
    },
    on(type, handler) {
        const handlers = this.all.get(type)
        if (handlers) {
            handlers.push(handler)
        } else {
            this.all.set(type, [handler])
        }
    },
    emit(type, ...args) {
        const handlers = this.all.get(type)
        if (handlers) {
            handlers.forEach(fn => fn(...args))
        }
    }
}


// console.log(bus)

/**
 * global variable
 * @var pdfjsLib
 */

export default class PDFViewApp {
    $container = null
    $btn_expand_sample = null
    $btn_search = null
    $txt_current_page = null
    $txt_total_page = null
    $btn_prev_page = null
    $btn_next_page = null
    $btn_zoom_out = null
    $btn_zoom_in = null
    $txt_zoom_value = null
    $zoom_dropdown = null
    $btn_screenfull = null
    $pdfviewer_bd_sample = null
    $pdfviewer_bd_pages = null
    $notice_msg = null
    $loadingbar = null

    sampleExpand = false
    #appSample = null
    #appPages = null
    eventList = []
    pdf = null
    // cacheZoom = -1
    constructor(container) {
        if (!container) {
            console.error('container is null')
            return
        }
        this.$container = container
        this.$btn_expand_sample = container.querySelector('.btn_expand_sample')
        this.$txt_current_page = container.querySelector('.txt_current_page')
        this.$txt_total_page = container.querySelector('.txt_total_page')
        this.$btn_prev_page = container.querySelector('.btn_prev_page')
        this.$btn_next_page = container.querySelector('.btn_next_page')
        this.$btn_zoom_out = container.querySelector('.btn_zoom_out')
        this.$btn_zoom_in = container.querySelector('.btn_zoom_in')
        this.$txt_zoom_value = container.querySelector('.txt_zoom_value')
        this.$zoom_dropdown = container.querySelector('.zoom-dropdown ul')
        this.$pdfviewer_bd_sample = container.querySelector('.pdfviewer-bd-sample')
        this.$pdfviewer_bd_pages = container.querySelector('.pdfviewer-bd-pages')
        this.$btn_screenfull = container.querySelector('.btn_screenfull')
        // this.$btn_search = container.querySelector('.btn_search')
        this.$notice_msg = container.querySelector('.notice')
        this.$loadingbar = container.querySelector('.loadingbar')
        // console.log(this)
    }

    start(opts) {
        if (!opts) {
            console.error('opts is null')
            return
        }
        const {
            page: jumpPageNum,
            url
        } = opts
        if (!url) {
            console.error('url is null')
            return
        }
        try {
            const loadingTask = pdfjsLib.getDocument({
                url,
                cMapUrl: config.cMapUrl,
                cMapPacked: true,
            })
            loadingTask.promise.then((pdf) => {
                this.pdf = Object.freeze(pdf)
                this.#appPages = new PDFViewerPages(
                    pdf,
                    this.$pdfviewer_bd_pages,
                    this,
                    this.$loadingbar,
                    jumpPageNum
                )
                this.updateTotalPage(pdf.numPages)
                if (jumpPageNum) {
                    this.updateCurrentPage(jumpPageNum)
                }
            })
        } catch (error) {
            alert(error.message)
        }
        this.initEventList()
        this.bindEvent()
    }
    initEventList() {
        const that = this
        const useCallback = (callback) => {
            return (e) => {
                e.stopPropagation()
                e.preventDefault()
                if (that.#appPages.taskExecStatus) {
                    // alert('加载页面中，请稍后！')
                    return
                }
                callback(e)
            }
        }
        this.eventList = [
            {
                el: this.$btn_expand_sample,
                type: 'click',
                callback: useCallback(() => {
                    this.toggleSampleExpand()
                    this.$btn_expand_sample.classList.toggle('active')
                    this.loadSample()
                })
            },
            {
                el: this.$pdfviewer_bd_sample,
                type: 'click',
                callback: useCallback((e) => {
                    const el = e.target
                    if (el.classList.contains('pdfviewer-page')) {
                        const index = +el.dataset.index
                        this.#appPages.movePage(index)
                        const page = index + 1
                        this.updateCurrentPage(page)
                    }
                })
            },
            {
                el: this.$container,
                type: 'click',
                callback: useCallback((e) => {
                    const el = e.target
                    const isSample = this.$pdfviewer_bd_sample.contains(el)
                        || this.$btn_expand_sample.contains(el)
                    if (!isSample && this.sampleExpand) {
                        this.toggleSampleExpand()
                    }
                })
            },
            {
                el: this.$pdfviewer_bd_pages,
                type: 'scroll',
                callback: useCallback(() => {
                    this.#appPages.updateCurrentIndex()
                    this.updateCurrentPage(this.#appPages.currentIndex + 1)
                })
            },
            {
                el: this.$btn_prev_page,
                type: 'click',
                callback: useCallback(() => {
                    this.#appPages.prevPage()
                })
            },
            {
                el: this.$btn_next_page,
                type: 'click',
                callback: useCallback(() => {
                    this.#appPages.nextPage()
                })
            },
            {
                el: this.$btn_zoom_out,
                type: 'click',
                callback: useCallback(() => {
                    this.#appPages.zoomOutPage()
                })
            },
            {
                el: this.$btn_zoom_in,
                type: 'click',
                callback: useCallback(() => {
                    this.#appPages.zoomInPage()
                })
            },
            {
                el: this.$zoom_dropdown,
                type: 'click',
                callback: useCallback((e) => {
                    const el = e.target
                    if (el.nodeName === 'LI') {
                        if (!this.#appPages.taskExecStatus) {
                            const value = +el.dataset.value
                            // if (this.cacheZoom === value) {
                            //     return
                            // }
                            // this.cacheZoom = value
                            this.#appPages.updateZoom(value)
                            this.updateZoomValue(value)
                        }
                    }
                })
            },
            {
                el: this.$btn_screenfull,
                type: 'click',
                callback: useCallback(() => {
                    // toggleFull(this.$container, null, () => {
                    //     this.#appPages.updateZoom(-1)
                    // })
                    screenfull.toggle().then((e) => {
                        this.$btn_screenfull.classList.toggle('is-screenfull')
                        console.log('screenfull.toggle')
                        this.#appPages.updateZoom(-1)
                    })
                    // this.cacheZoom = null
                })
            },
            // {
            //     el: this.$btn_search,
            //     type: 'click',
            //     callback: useCallback(() => {
            //         this.#appPages.$children.forEach((_, index) => {
            //             this.#appPages.doc.getPage(index + 1).then((page) => {
            //                 page.getTextContent().then(textContent => {
            //                     console.log('textContent', textContent)
            //                 })
            //             })
            //         })
            //     })
            // },
        ]
    }
    loadSample() {
        if (this.#appSample) {
            return
        }
        this.#appSample = new PDFViewerPages(this.pdf, this.$pdfviewer_bd_sample, this, this.$loadingbar)
    }

    bindEvent() {
        this.eventList.forEach(({ el, type, callback }) => {
            el.addEventListener(type, callback)
        })
    }
    unbindEvent() {
        this.eventList.forEach(({ el, type, callback }) => {
            el.removeEventListener(type, callback)
        })
    }
    toggleSampleExpand() {
        const status = this.sampleExpand
        const sampleEl = this.$pdfviewer_bd_sample
        const toggle = (name) => sampleEl.classList.toggle(name)
        if (status) {
            toggle('animate')
            sampleEl.offsetWidth
            setTimeout(() => toggle('show'), 100)
        } else {
            toggle('show')
            sampleEl.offsetWidth
            setTimeout(() => toggle('animate'), 0)
        }
        this.sampleExpand = !status
    }
    updateCurrentPage(page) {
        this.$txt_current_page.innerHTML = page
    }
    updateTotalPage(numPages) {
        this.$txt_total_page.innerHTML = numPages
    }
    updateZoomValue(v) {
        this.$txt_zoom_value.innerHTML = v === -1 ? '自适应' : (v * 100) + '%'
    }
}

class PDFViewerPages {
    $container = null // 当前容器
    $innerContainer = null // 当前内部容器
    $children = null // 所有子页面
    $loadingbar = null

    #observer = null // 观察器
    #parent = null // 父类

    doc = null // pdf文档对象
    numPages = null // 总页数
    currentIndex = 0 // 当前下标，页号为下标+1
    scale = 1 // 当前缩放比例
    initScale = 1
    zoomRatio = 0.2
    isInitRender = false
    maxH5Width = 540
    constructor(
        pdf,
        container,
        parent,
        loadingbar,
        jumpPageNum
    ) {
        this.doc = Object.freeze(pdf)
        this.numPages = pdf.numPages
        this.$container = container
        this.$innerContainer = container.querySelector('.pdfviewer-pages-inner')
        this.#parent = parent
        this.$loadingbar = loadingbar
        this.initPage()
        this.$children = [].slice.call(
            container.querySelectorAll('.pdfviewer-page')
        )
        // this.rendered = new Array(this.$children.length).fill(0)
        this.initViewport().then(() => {
            this.#observer = new ViewerObserver(container, this.$children)
        })
        bus.on(bus.eTypes.render_page, (pageIndex) => {
            if (!this.isInitRender) {
                this.isInitRender = true
                if (jumpPageNum) {
                    this.movePage(jumpPageNum - 1)
                }
            }
            console.log('触发了 render_page')
            this.renderPage(this.$children[pageIndex], pageIndex, () => {
                bus.emit(bus.eTypes.exec_task)
            })
        })
    }
    get taskExecStatus() {
        return this.#observer?.status
    }
    // 初始化页面
    initPage() {
        const rc = []
        for (let i = 0; i < this.numPages; i++) {
            const page = (i + 1).toString().padStart(2, '0')
            const s = `<div class="pdfviewer-page"
                data-index="${i}"
                data-page="${page}"
                data-rendered="0"
                data-show="0">
            </div>`
            rc.push(s)
        }
        this.$innerContainer.innerHTML = rc.join('')
    }
    // 初始化做到自适应页面宽度
    initViewport() {
        return this.doc.getPage(1).then(page => {
            const { width, height, newScale } = this.getPdfViewport(page)
            this.scale = newScale
            this.initScale = newScale
            this.$children.forEach((el) => {
                el.style.width = width + "px"
                el.style.height = height + "px"
            })
            this.setNewScale(newScale)
            return Promise.resolve()
        })
    }

    getPdfViewport(page, scale = -1) {
        let newScale = scale === -1 ? 1 : scale
        let viewport = page.getViewport({ scale: newScale })
        // 自适应模式
        if (scale === -1) {
            const innerWidth = this.$innerContainer.getBoundingClientRect().width
            console.log(`innerWidth: ${innerWidth}; viewport.width: ${viewport.width};`)
            if (innerWidth) {
                newScale = Math.floor((innerWidth / viewport.width) * 100) / 100
                viewport = page.getViewport({ scale: newScale })
            }
        }
        const width = Math.floor(viewport.width)
        const height = Math.floor(viewport.height)
        console.log(`width: ${width}; height: ${height}; newScale: ${newScale};`)
        // Support HiDPI-screens.
        const outputScale = window.devicePixelRatio || 1;
        const rc = { width, height, newScale, outputScale, viewport }
        return rc
    }

    getCanvas(el) {
        let canvas = el.querySelector('canvas')
        if (!canvas) {
            canvas = document.createElement('canvas')
            canvas.className = 'page-canvas'
            el.appendChild(canvas)
        }
        const context = canvas.getContext('2d', { willReadFrequently: true })
        // context.canvas.willReadFrequently = true
        return [canvas, context]
    }
    /**
     * 渲染PDF
     * @param {Element} el 
     * @param {number} pageIndex 
     * @param {Function} next 
     */
    renderPage = (el, pageIndex, next) => {
        if (el.dataset.rendered === '1') {
            next()
            return
        }
        // 设置渲染状态
        el.dataset.rendered = '1'
        this.setLoading(true)
        const [canvas, context] = this.getCanvas(el)
        const pageNumber = pageIndex + 1
        this.doc.getPage(pageNumber).then((page) => {
            readCompleted(page)
        }, (reason) => {
            console.error(`PDF loading error: ${reason}`)
            next()
        })
        const readCompleted = (page) => {
            const { width, height, outputScale, viewport } = this.getPdfViewport(page, this.scale)
            el.style.width = width + "px"
            el.style.height = height + "px"
            canvas.width = Math.floor(viewport.width * outputScale);
            canvas.height = Math.floor(viewport.height * outputScale);
            canvas.style.width = Math.floor(viewport.width) + "px";
            canvas.style.height = Math.floor(viewport.height) + "px";
            // https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/transform
            const transform = outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null;
            const renderContext = {
                canvasContext: context,
                transform: transform,
                viewport: viewport
            };
            const renderTask = page.render(renderContext)
            renderTask.promise.then(() => {
                this.setLoading(false)
                console.log('Page rendered')
                next()
            }, () => next())
            renderText(page, viewport)
        }
        // 渲染文本
        const renderText = (page, viewport) => {
            page.getTextContent().then(textContent => {
                let textEl = el.querySelector('.textLayer')
                if (!textEl) {
                    textEl = document.createElement('div')
                    textEl.className = 'textLayer'
                    el.appendChild(textEl)
                }
                textEl.innerHTML = ''
                pdfjsLib.renderTextLayer({
                    textContentSource: textContent,
                    textContent,
                    container: textEl,
                    viewport,
                    textDivs: []
                })
            })
        }
    }
    // 这里更新`--scale-factor`
    setNewScale(newScale) {
        this.$container.style.setProperty('--scale-factor', newScale)
    }
    setLoading(show = true) {
        this.$loadingbar.style.display = show ? 'flex' : 'none'
    }
    updateCurrentIndex() {
        // 便宜量 - 已滚动高度 <= 视口，即将/已出现在视口内
        // 已滚动高度
        const scrollTop = +this.$container.scrollTop.toFixed(2)
        let only = 0
        this.$children.forEach((el, i) => {
            const top = el.offsetTop // 距离顶部得偏移量
            const threshold = el.offsetHeight * .5 // 偏移量
            const diff = top - scrollTop
            console.log(`i:${i}; top:${top}; scrollTop:${scrollTop}; diff:${diff}; threshold:${threshold};`)
            if (diff <= threshold) {
                only = i
            }
        })
        this.currentIndex = only
        const p = only + 1
        this.#parent.updateCurrentPage(p)
    }
    movePage(index) {
        const el = this.$children[index]
        const top = el.offsetTop
        // https://developer.mozilla.org/zh-CN/docs/Web/API/Element/scrollTo
        // @FIXME: `behavior:"smooth"` 设置此属性有问题，位置有偏差；scrollIntoView方法同理
        this.$container.scrollTo({ top })
    }
    prevPage() {
        if (this.currentIndex > 0) {
            this.currentIndex -= 1
            this.movePage(this.currentIndex)
        }
    }
    nextPage() {
        if (this.currentIndex < this.numPages) {
            this.currentIndex += 1
            this.movePage(this.currentIndex)
        }
    }
    zoomOutPage() {
        this.scale += this.zoomRatio
        this.setNewScale(this.scale)
        this.rerender()
    }
    zoomInPage() {
        const innerWidth = this.$innerContainer.getBoundingClientRect().width
        if (innerWidth < this.maxH5Width) {
            if (this.scale <= this.initScale) {
                return
            }
        }
        this.scale -= this.zoomRatio
        this.setNewScale(this.scale)
        this.rerender()
    }
    updateZoom(value) {
        if (value === -1) {
            this.initViewport().then(() => this.rerender())
        } else {
            this.scale = value
            this.setNewScale(this.scale)
            this.rerender()
        }
    }
    // 重新渲染
    rerender() {
        // 首先清空未执行得任务
        this.taskQueue = []
        this.$children.forEach((el, i) => {
            // 未渲染的页面不处理
            if (el.dataset.rendered === '0') {
                return
            }
            el.dataset.rendered = '0'
            this.#observer.postTask(i)
        })
    }
}

class ViewerObserver {
    observer = null
    status = false
    taskQueue = []
    constructor(rootEl, children) {
        if (this.observer) {
            return
        }
        this.observer = new IntersectionObserver((entries) => {
            // 使用 intersectionRatio ，快速滚动时正常，但慢慢滚动时，实际就不生效，出现白屏
            // 判断显隐用 isIntersecting 更直观。如果使用 intersectionRatio>0 判断需要配合 threshold 配置，不然出现白屏
            entries.forEach(item => {
                const el = item.target
                // 获取当前页号
                const index = el.dataset.index
                // console.log(`isIntersecting:${item.isIntersecting}, index:${index}`)
                // 元素出现可视区域
                if (item.isIntersecting) {
                    this.postTask(index)
                    el.dataset.show = 1
                } else {
                    this.removeTaskQueue(index)
                    el.dataset.show = 0
                }
            })
        }, {
            root: rootEl,
            // 阀值设为 1，当只有比例达到 1 时才触发回调函数
            // 为0时也需要触发，删除队列
            // threshold: .25,
        })
        children.forEach(el => this.observer.observe(el))
        bus.on(bus.eTypes.exec_task, () => {
            console.log('触发了 exec_task')
            this.status = false
            this.execTask()
        })
    }
    // 发布任务
    postTask(index) {
        const i = +index
        this.removeTaskQueue(i)
        this.addTaskQueue(i)
        this.execTask()
    }
    // 删除队列
    removeTaskQueue(index) {
        const taskIndex = this.taskQueue.findIndex(i => i === index)
        if (taskIndex !== -1) {
            this.taskQueue.splice(taskIndex, 1)
        }
        return taskIndex
    }
    // 添加任务队列
    addTaskQueue(index) {
        this.taskQueue.push(index)
    }
    // 执行任务
    execTask() {
        if (this.status) {
            return
        }
        this.status = true
        if (!this.taskQueue.length) {
            this.status = false
            return
        }
        // 始终保持优先执行原则
        const pageIndex = this.taskQueue.pop()
        bus.emit(bus.eTypes.render_page, pageIndex)
    }
}